package main.java.sokoban;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.Stack;

public class GamePanel extends JPanel implements KeyListener {

    // 定义方块的大小，这里每个方块是45x45像素
    private final int TILE_SIZE = 45;
    // 二维字符数组，用于存储地图信息
    private char[][] map;
    // 玩家在地图上的行和列位置
    private int playerRow, playerCol;
    // 用于保存地图的历史状态，以便于实现撤销功能
    private final Stack<char[][]> mapHistory = new Stack<>();
    // 用于保存玩家位置的历史状态，以便于实现撤销功能
    private final Stack<int[]> playerHistory = new Stack<>();
    // 撤销按钮
    private JButton undoButton;
    // 当前关卡
    private int currentLevel = 1;

    // 图片资源
    private final Image playerUp = new ImageIcon(getClass().getClassLoader().getResource("player_up.png")).getImage();
    private final Image playerDown = new ImageIcon(getClass().getClassLoader().getResource("player_down.png")).getImage();
    private final Image playerLeft = new ImageIcon(getClass().getClassLoader().getResource("player_left.png")).getImage();
    private final Image playerRight = new ImageIcon(getClass().getClassLoader().getResource("player_right.png")).getImage();
    // 当前玩家的图片，初始为向下
    private Image currentPlayerImage = playerDown;

    // 墙、箱子、目标点和地板的图片资源
    private final Image wallImg = new ImageIcon(getClass().getClassLoader().getResource("wall.png")).getImage();
    private final Image boxImg = new ImageIcon(getClass().getClassLoader().getResource("box.png")).getImage();
    private final Image goalImg = new ImageIcon(getClass().getClassLoader().getResource("goal.png")).getImage();
    private final Image floorImg = new ImageIcon(getClass().getClassLoader().getResource("floor.png")).getImage();
    private final Image goal_boxImg = new ImageIcon(getClass().getClassLoader().getResource("goal_box.png")).getImage();
    // 背景图片
    private final Image backgroundImg = new ImageIcon(getClass().getClassLoader().getResource("background.png")).getImage();

    /**
     * 构造函数，初始化游戏面板
     *
     * @param levelMap 二维字符数组，表示地图信息
     */
    public GamePanel(char[][] levelMap,int level) {
        this.map = levelMap;
        this.currentLevel = level;
        // 设置面板的首选尺寸，宽度为地图列数乘以方块大小，高度为地图行数乘以方块大小
        setPreferredSize(new Dimension(map[0].length * TILE_SIZE, map.length * TILE_SIZE));
        // 设置面板可聚焦，以便于接收键盘事件
        setFocusable(true);
        // 添加键盘监听器
        addKeyListener(this);
        // 查找玩家的初始位置
        findPlayer();
        //设置绝对布局
        setLayout(null);
        // 添加撤销按钮
        addUndoButton();
        // 添加退出按钮
        addExitButton();
        // 添加选择关卡按钮
        addLevelSelectButtons();
        // 请求焦点
        requestFocusInWindow();
    }

    // 添加撤销按钮
    private void addUndoButton() {
        ImageIcon undoIcon = new ImageIcon(getClass().getClassLoader().getResource("step_back.png"));
        Image scaledUndoIcon = undoIcon.getImage().getScaledInstance(90, 45, Image.SCALE_SMOOTH);
        //设置回退一步按钮位置和尺寸
        JButton undoButton = new JButton(new ImageIcon(scaledUndoIcon));
        undoButton.setBounds(157, 10, 90, 45); // 左上角位置，图片大小自己调
        undoButton.setBorderPainted(false);      // 不画边框
        undoButton.setContentAreaFilled(false);  // 不画背景
        undoButton.setFocusPainted(false);// 不显示选中轮廓
        undoButton.setFocusable(false);
        undoButton.setToolTipText("撤回一步");
        undoButton.addActionListener(e -> {
            undoMove();//添加撤回功能
            requestFocusInWindow();//获取焦点
            repaint();    // 重绘界面
        });
        add(undoButton);
    }

    // 添加退出按钮
    private void addExitButton(){
        ImageIcon exitIcon = new ImageIcon(getClass().getClassLoader().getResource("exit.png"));
        Image scaledExitIcon = exitIcon.getImage().getScaledInstance(60, 45, Image.SCALE_SMOOTH);
        JButton exitButton = new JButton(new ImageIcon(scaledExitIcon));
        exitButton.setBounds(10,10,60,45);
        exitButton.setBorderPainted(false);
        exitButton.setContentAreaFilled(false);
        exitButton.setFocusPainted(false);
        exitButton.setFocusable(false);
        exitButton.setToolTipText("退出");
        exitButton.addActionListener(e -> System.exit(0));
        add(exitButton);
    }

    //添加关卡选择按钮
    private void addLevelSelectButtons() {
        ImageIcon levelSelectIcon = new ImageIcon(getClass().getClassLoader().getResource("level_select.png"));
        Image scaledImage = levelSelectIcon.getImage().getScaledInstance(60, 45, Image.SCALE_SMOOTH);
        JButton levelSelectButton = new JButton(new ImageIcon(scaledImage));
        levelSelectButton.setBounds(335,10,60,45);
        levelSelectButton.setBorderPainted(false);
        levelSelectButton.setContentAreaFilled(false);
        levelSelectButton.setFocusPainted(false);
        levelSelectButton.setFocusable(false);
        levelSelectButton.setToolTipText("关卡选择");
        levelSelectButton.addActionListener(e -> {
            JFrame topFrame = (JFrame) SwingUtilities.getWindowAncestor(this);
            LevelSelectPanel selectPanel = new LevelSelectPanel(topFrame);
            topFrame.setContentPane(selectPanel);
            topFrame.pack();
            topFrame.setLocationRelativeTo(null);
            topFrame.validate();
        });
        add(levelSelectButton);
    }

    /**
     * 在地图中查找玩家的位置
     * 该方法通过遍历二维字符数组map来寻找代表玩家的字符'P'
     * 找到玩家后，将玩家所在的行和列分别存储在playerRow和playerCol变量中
     * 注意：此方法假设地图中只有一个玩家
     */
    private void findPlayer() {
        // 遍历地图的每一行和每一列
        for (int row = 0; row < map.length; row++)
            for (int col = 0; col < map[0].length; col++)
                // 检查当前位置是否为玩家
                if (map[row][col] == 'P' || map[row][col] == 'Q') {
                    // 更新玩家的行和列位置
                    playerRow = row;
                    playerCol = col;
                    // 找到玩家后立即返回，避免不必要的搜索
                    return;
                }
    }


    @Override
    protected void paintComponent(Graphics g) {
        // 调用父类的paintComponent方法以确保组件绘制正确
        super.paintComponent(g);
        // 绘制背景图片
        g.drawImage(backgroundImg, 0, 0, getWidth(), getHeight(), this);

        // 遍历地图数组
        for (int row = 0; row < map.length; row++) {
            for (int col = 0; col < map[0].length; col++) {
                int x = col * TILE_SIZE;
                int y = row * TILE_SIZE;
                char tile = map[row][col];

                // 画地板或目标
                if (tile == 'G') {
                    g.drawImage(goalImg, x, y, TILE_SIZE, TILE_SIZE, this);
                } else if (tile != '*') {
                    g.drawImage(floorImg, x, y, TILE_SIZE, TILE_SIZE, this);
                }

                // 根据不同的地图字符绘制相应的图片
                switch (tile) {
                    case '#':
                        g.drawImage(wallImg, x, y, TILE_SIZE, TILE_SIZE, this);
                        break;
                    case 'B':
                        g.drawImage(boxImg, x, y, TILE_SIZE, TILE_SIZE, this);
                        break;
                    case '*':
                        g.drawImage(backgroundImg, x, y, TILE_SIZE, TILE_SIZE, this);
                        break;
                    case 'P':
                        g.drawImage(currentPlayerImage, x, y, TILE_SIZE, TILE_SIZE, this);
                        break;
                    case 'O':
                        g.drawImage(goal_boxImg, x, y, TILE_SIZE, TILE_SIZE, this);
                        break;
                    case 'Q':
                        g.drawImage(goalImg, x, y, TILE_SIZE, TILE_SIZE, this);
                        g.drawImage(currentPlayerImage, x, y, TILE_SIZE, TILE_SIZE, this);
                }
                g.setColor(Color.BLACK);
                g.drawRect(x, y, TILE_SIZE, TILE_SIZE);
            }
        }
    }

    /**
     * 保存当前游戏状态
     * 本方法通过复制当前地图和玩家位置信息，然后将这些信息压入历史状态栈中
     * 这样做是为了在需要时能够回退到之前的状态，或者重置游戏
     */
    private void saveState() {
        // 创建一个二维字符数组来存储当前地图的状态
        char[][] copy = new char[map.length][map[0].length];
        // 遍历地图，克隆每个一维数组到新的二维数组中
        for (int i = 0; i < map.length; i++)
            copy[i] = map[i].clone();

        // 将复制的地图状态压入地图历史状态栈中
        mapHistory.push(copy);
        // 将玩家当前位置压入玩家历史位置栈中
        playerHistory.push(new int[]{playerRow, playerCol});
    }


    /**
     * 移动玩家位置
     *
     * @param dRow         玩家在行方向上的移动增量
     * @param dCol         玩家在列方向上的移动增量
     * @param directionImg 表示玩家移动方向的图片
     */
    private void movePlayer(int dRow, int dCol, Image directionImg) {
        // 计算玩家的新位置
        int newRow = playerRow + dRow;
        int newCol = playerCol + dCol;
        // 获取目的地的地形
        char dest = map[newRow][newCol];

        // 更新玩家的当前图片为移动方向的图片
        currentPlayerImage = directionImg;

        // 如果目的地是空地或目标点
        if (dest == ' ' || dest == 'G') {
            // 保存当前游戏状态
            saveState();
            // 将玩家原来的当前位置重置
            if(map[playerRow][playerCol] == 'Q' || map[playerRow][playerCol] == 'G'){
                map[playerRow][playerCol] = 'G';
            }else {
                map[playerRow][playerCol] = ' ';
            }
            // 更新玩家的位置为新位置
            playerRow = newRow;
            playerCol = newCol;
            // 将新位置标记为玩家位置
            if(map[playerRow][playerCol] == 'G'){
                map[playerRow][playerCol] = 'Q';
            }else{
                map[playerRow][playerCol] = 'P';
            }
        } else if (dest == 'B' || dest == 'O') {
            // 如果目的地是箱子或箱子在目标点上，计算箱子的新位置
            int boxNewRow = newRow + dRow;
            int boxNewCol = newCol + dCol;
            // 获取箱子目的地的地形
            char boxDest = map[boxNewRow][boxNewCol];

            // 如果箱子目的地是空地或目标点
            if (boxDest == ' ' || boxDest == 'G') {
                // 保存当前游戏状态
                saveState();

                // 更新箱子的位置
                map[boxNewRow][boxNewCol] = (boxDest == 'G') ? 'O' : 'B';
                // 将玩家原来的当前位置重置
                if(map[playerRow][playerCol] == 'Q'){
                    map[playerRow][playerCol] = 'G';
                }else {
                    map[playerRow][playerCol] = ' ';
                }
                // 更新玩家的位置为新位置
                playerRow = newRow;
                playerCol = newCol;
                // 将新位置标记为玩家位置
                if(map[playerRow][playerCol] == 'O'){
                    map[playerRow][playerCol] = 'Q';
                }else{
                    map[playerRow][playerCol] = 'P';
                }
            }
        }

        // 检查是否胜利
        SwingUtilities.invokeLater(() -> checkWin());
        // 重新绘制界面
        repaint();
    }


    /**
     * 撤销移动操作
     * 此方法用于将游戏恢复到之前的某个状态，以允许玩家撤销他们的最后一步移动
     * 它通过从地图历史记录和玩家位置历史记录中弹出最近的记录来实现
     */
    private void undoMove() {
        // 如果没有历史记录可撤销，则直接返回
        if (mapHistory.isEmpty()) return;
        // 从历史记录中获取最近的地图状态
        map = mapHistory.pop();
        // 从历史记录中获取玩家的最近位置
        int[] pos = playerHistory.pop();
        // 更新玩家的行位置
        playerRow = pos[0];
        // 更新玩家的列位置
        playerCol = pos[1];
        // 重新绘制界面以反映撤销后的状态
        repaint();
    }

    /**
     * 检查游戏是否胜利
     * 此方法遍历整个地图，检查是否存在目标点（'G'）
     * 如果不存在目标点，则认为玩家已经赢得游戏，并显示胜利对话框
     */
    private void checkWin() {
        // 遍历地图的每一行和每一列
        for (int row = 0; row < map.length; row++)
            for (int col = 0; col < map[0].length; col++)
                // 如果找到目标点，表明游戏尚未胜利，直接返回
                if (map[row][col] == 'G' || map[row][col] == 'Q')
                    return;
        // 如果没有找到目标点，显示胜利消息对话框
        int option = JOptionPane.showOptionDialog(
                this,
                "你赢啦！",
                "胜利",
                JOptionPane.YES_NO_CANCEL_OPTION,
                JOptionPane.INFORMATION_MESSAGE,
                null,
                new String[]{"下一关","重新开始","退出"},
                "下一关"
        );
        if(option == 0){
            //下一关
            goToNextLevel();
        } else if (option == 1) {
            //重新开始本关
            restartLevel();
        }else{
            //退出游戏
            System.exit(0);
        }
    }

    public void goToNextLevel(){
        int nextLevel = currentLevel + 1;
        char[][] nextMap = LevelManager.getLevel(nextLevel);
        if(nextMap != null){
            JFrame topFrame = (JFrame) SwingUtilities.getWindowAncestor(this);
            topFrame.setContentPane(new GamePanel(nextMap, nextLevel));
            topFrame.validate();
        }else{
            JOptionPane.showMessageDialog(this, "没有更多关卡了！");
        }
    }
    public void restartLevel(){
        JFrame topFrame = (JFrame) SwingUtilities.getWindowAncestor(this);
        topFrame.setContentPane(new GamePanel(LevelManager.getLevel(currentLevel), currentLevel));
        topFrame.validate();
    }


    /**
     * 处理键盘按下事件的方法
     * 根据按键的不同，调用相应的玩家移动方法或撤销移动方法
     *
     * @param e 键盘事件对象，包含按键信息
     */
    @Override
    public void keyPressed(KeyEvent e) {
        switch (e.getKeyCode()) {
            case KeyEvent.VK_UP -> movePlayer(-1, 0, playerUp); // 当按下上箭头键时，调用movePlayer方法使玩家向上移动
            case KeyEvent.VK_DOWN -> movePlayer(1, 0, playerDown); // 当按下下箭头键时，调用movePlayer方法使玩家向下移动
            case KeyEvent.VK_LEFT -> movePlayer(0, -1, playerLeft); // 当按下左箭头键时，调用movePlayer方法使玩家向左移动
            case KeyEvent.VK_RIGHT -> movePlayer(0, 1, playerRight); // 当按下右箭头键时，调用movePlayer方法使玩家向右移动
        }
    }

    /**
     * 处理键盘释放事件的方法
     * 本例中未使用具体实现，留空
     *
     * @param e 键盘事件对象，未使用
     */
    @Override
    public void keyReleased(KeyEvent e) {
    }

    /**
     * 处理键盘输入事件的方法
     * 本例中未使用具体实现，留空
     *
     * @param e 键盘事件对象，未使用
     */
    @Override
    public void keyTyped(KeyEvent e) {
    }

}